%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Functions on sets of {name, ref} pairs. Primarily used to represent
%%% field name to/from field ref pairs.
%%%
%%% Set operations on refs are used in sequencing, which is why we maintain
%%% a distinct set of refs in the representation.
%%%
%%% @headerfile "../include/txm.hrl"

-module(nr_set).

-include("txm.hrl").

%% Exports
-export([new/0, get_by_name/2, get_by_ref/2, store/2]).
-export([delete_by_name/2, delete_by_ref/2]).
-export([name_for_ref/2, ref_for_name/2]).
-export([count/1]).
-export([dict_by_name/1, dict_by_ref/1, ref_set/1, name_set/1]).
-export([from_proplist/1, from_dict_key_name/1, from_dict_key_ref/1]).
-export([to_proplist_by_name/2, to_proplist_by_ref/2]).
-export([fold/3, map/2]).
-export([is_nr_set/1]).

%% @doc Returns a new, empty input/output set.
%% @spec new() -> nr_set()
-spec(new() -> nr_set()).
new() ->
  #?NR_SET{}.

%% @doc Returns the {Name, Ref} pair matching the supplied `Name'.
%% @spec get_by_name(string(), nr_set()) -> nr_entry() | not_found()
-spec(get_by_name(string(), nr_set()) -> nr_entry() | not_found()).
get_by_name(Name, #?NR_SET{n2r = N2R}) ->
  case dict:find(Name, N2R) of
    {ok, Ref} -> {Name, Ref};
    error -> ?NOT_FOUND
  end.

%% @doc Returns the {Name, Ref} pair matching the supplied `Ref'.
%% @spec get_by_ref(ref(), nr_set()) -> nr_entry()| not_found()
-spec(get_by_ref(ref(), nr_set()) -> nr_entry() | not_found()).
get_by_ref(Ref, #?NR_SET{r2n = R2N}) ->
  case dict:find(Ref, R2N) of
    {ok, Name} -> {Name, Ref};
    error -> ?NOT_FOUND
  end.

%% @doc Returns the ref for the supplied `Name'.
%% @spec ref_for_name(string(), nr_set()) -> ref() | not_found()
-spec(ref_for_name(string(), nr_set()) -> ref() | not_found()).
ref_for_name(Name, #?NR_SET{n2r = N2R}) ->
  case dict:find(Name, N2R) of
    {ok, Ref} -> Ref;
    error -> ?NOT_FOUND
  end.

%% @doc Returns the name for the supplied `Ref'.
%% @spec name_for_ref(ref(), nr_set()) -> string() | not_found()
-spec(name_for_ref(ref(), nr_set()) -> string() | not_found()).
name_for_ref(Ref, #?NR_SET{r2n = R2N}) ->
  case dict:find(Ref, R2N) of
    {ok, Name} -> Name;
    error -> ?NOT_FOUND
  end.

%% @doc Returns the nr_set that results from adding the supplied `Value'
%% into it. If either the name or the ref or both match one or even two
%% existing entries, those are deleted first.
%% #1
%% @spec store(nr_entry() | list(), nr_set()) -> nr_set()
-spec(store(nr_entry() | list(), nr_set()) -> nr_set()).
store({Name, Ref}, #?NR_SET{} = NrSet) ->
  Temp1 = nr_set:delete_by_name(Name, NrSet),
  #?NR_SET{n2r = N2R,
           r2n = R2N,
           refSet = RefSet} = nr_set:delete_by_ref(Ref, Temp1),

  #?NR_SET{n2r = dict:store(Name, Ref, N2R),
           r2n = dict:store(Ref, Name, R2N),
           refSet = sets:add_element(Ref, RefSet)};

%% @doc Returns the nr_set that results for adding each value in the
%% list.
%% #2
store([], #?NR_SET{} = NrSet) -> NrSet;
store([First | Rest], NrSet) ->
  NewSet = store(First, NrSet),
  store(Rest, NewSet).

%% @doc Returns the size of the nr_set.
%% @spec count(nr_set()) -> integer()
-spec(count(nr_set()) -> integer()).
count(#?NR_SET{refSet = RefSet}) ->
  sets:size(RefSet).

%% @doc Returns the nr_set that results from deleting the entry whose name
%% is `Name', if any.
%% @spec delete_by_name(string(), nr_set()) -> nr_set()
-spec(delete_by_name(string(), nr_set()) -> nr_set()).
delete_by_name(Name, #?NR_SET{n2r = N2R,
                              r2n = R2N,
                              refSet = RefSet} = NrSet) ->
  case dict:find(Name, N2R) of
    {ok, Ref} ->
      #?NR_SET{n2r = dict:erase(Name, N2R),
               r2n = dict:erase(Ref, R2N),
               refSet = sets:del_element(Ref, RefSet)};
    error ->
      NrSet
  end.

%% @doc Returns the nr_set that results from deleting the entry whose ref
%% is `Ref', if any.
%% @spec delete_by_ref(ref(), nr_set()) -> nr_set()
-spec(delete_by_ref(ref(), nr_set()) -> nr_set()).
delete_by_ref(Ref, #?NR_SET{n2r = N2R,
                            r2n = R2N,
                            refSet = RefSet} = NrSet) ->
  case dict:find(Ref, R2N) of
    {ok, Name} ->
      #?NR_SET{n2r = dict:erase(Name, N2R),
               r2n = dict:erase(Ref, R2N),
               refSet = sets:del_element(Ref, RefSet)};
    error ->
      NrSet
  end.

%% @doc Returns a dictionary whose keys are name and values are ref entries
%% from the supplied nr_set.
%% @spec dict_by_name(nr_set()) -> dict()
-spec(dict_by_name(nr_set()) -> dict()).
dict_by_name(#?NR_SET{n2r = N2R}) ->
  N2R.

%% @doc Returns a dictionary whose keys are ref and values are name entries
%% from the supplied nr_set.
%% @spec dict_by_ref(nr_set()) -> dict()
-spec(dict_by_ref(nr_set()) -> dict()).
dict_by_ref(#?NR_SET{r2n = R2N}) ->
  R2N.

%% @doc Returns a set containing all the name entries in the supplied nr_set.
%% This is seldom required.
%% @spec name_set(nr_set()) -> set()
-spec(name_set(nr_set()) -> set()).
name_set(#?NR_SET{n2r = N2R}) ->
  sets:from_list(dict:fetch_keys(N2R)).

%% @doc Returns a set containing all the ref entries in the supplied nr_set.
%% This is frequently used.
%% @spec ref_set(nr_set()) -> set()
-spec(ref_set(nr_set()) -> set()).
ref_set(#?NR_SET{refSet = RefSet}) ->
  RefSet.

%% @doc Returns a new nr_set from 'Proplist' whose first tuple elements become
%% names and second tuple elements become refs.
%% @spec from_proplist(list()) -> nr_set()
-spec(from_proplist(list(nr_entry())) -> nr_set()).
from_proplist(Proplist) ->
  store(Proplist, new()).

%% @doc Returns a new nr_set from `Dict' whose keys become names and values
%% become refs.
%% @spec from_dict_key_name(Dict) -> nr_set()
-spec(from_dict_key_name(dict()) -> nr_set()).
from_dict_key_name(Dict) ->
  dict:fold(fun(Name, Ref, Acc) ->
                 nr_set:store({Name, Ref}, Acc)
            end, nr_set:new(), Dict).

%% @doc Returns a new nr_set from 'Dict' whose keys become refs and values
%% become names.
%% @spec from_dict_key_ref(dict()) -> nr_set()
-spec(from_dict_key_ref(dict()) -> nr_set()).
from_dict_key_ref(Dict) ->
  dict:fold(fun(Ref, Name, Acc) ->
              nr_set:store({Name, Ref}, Acc)
            end, nr_set:new(), Dict).

%% @doc Converts a proplist keyed by ref into one keyed by name, using
%% the supplied `NrSet'.
%% @spec to_proplist_by_name(list(), nr_set()) -> list()
-spec(to_proplist_by_name(list(), nr_set()) -> list()).
to_proplist_by_name(Proplist, NrSet) ->
  lists:foldl(fun({Ref, Value}, Acc) ->
                   Name = nr_set:name_for_ref(Ref, NrSet),
                   [{Name, Value} | Acc]
              end, [], Proplist).

%% @doc Converts a proplist keyed by name into one keyed by ref, using
%% the supplied `NrSet'.
%% @spec to_proplist_by_ref(list(), nr_set()) -> list()
-spec(to_proplist_by_ref(list(), nr_set()) -> list()).
to_proplist_by_ref(Proplist, NrSet) ->
  lists:foldl(fun({Name, Value}, Acc) ->
                   Ref = nr_set:ref_for_name(Name, NrSet),
                   [{Ref, Value} | Acc]
              end, [], Proplist).

%% @doc Folds the supplied `Fun/3' over the dictionary of names to refs and
%% returns the resulting accumulator.
%% The fun spec is `fun(name(), ref(), term()) -> term()' where
%% term() is the accumulator.
%% @spec fold(fun(), term(), nr_set()) -> term()
-spec(fold(fun(), term(), nr_set()) -> term()).
fold(Fun, InitialAcc, #?NR_SET{n2r = N2R}) ->
  dict:fold(Fun, InitialAcc, N2R).

%% @doc Returns the new nr_set with a new ref for each name as returned
%% by `Fun(Name, Ref)'.
%% @spec map(fun(), nr_set()) -> nr_set()
-spec(map(fun(), nr_set()) -> nr_set()).
map(Fun, #?NR_SET{n2r = N2R}) ->
  NewN2R = dict:map(Fun, N2R),
  from_dict_key_name(NewN2R).

%% @doc Returns true if the term is an nr_set, false otherwise.
%% @spec is_nr_set(term()) -> boolean()
-spec(is_nr_set(term()) -> boolean()).
is_nr_set(Term) ->
  is_record(Term, ?NR_SET).
